package net.osmand.plus.views; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.PointF; import android.graphics.RectF; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.util.DisplayMetrics; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.widget.Toast; import net.osmand.PlatformUtil; import net.osmand.access.AccessibilityActionsProvider; import net.osmand.core.android.MapRendererView; import net.osmand.data.LatLon; import net.osmand.data.QuadPoint; import net.osmand.data.QuadPointDouble; import net.osmand.data.RotatedTileBox; import net.osmand.map.IMapLocationListener; import net.osmand.map.MapTileDownloader.DownloadRequest; import net.osmand.map.MapTileDownloader.IMapDownloaderCallback; import net.osmand.plus.OsmAndConstants; import net.osmand.plus.OsmAndFormatter; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandSettings; import net.osmand.plus.R; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.helpers.TwoFingerTapDetector; import net.osmand.plus.views.MultiTouchSupport.MultiTouchZoomListener; import net.osmand.plus.views.OsmandMapLayer.DrawSettings; import net.osmand.util.MapUtils; import org.apache.commons.logging.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class OsmandMapTileView implements IMapDownloaderCallback { private static final int MAP_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 4; private static final int MAP_FORCE_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 5; private static final int BASE_REFRESH_MESSAGE = OsmAndConstants.UI_HANDLER_MAP_VIEW + 3; protected final static int LOWEST_ZOOM_TO_ROTATE = 9; private boolean MEASURE_FPS = false; private FPSMeasurement main = new FPSMeasurement(); private FPSMeasurement additional = new FPSMeasurement(); private View view; private Activity activity; private OsmandApplication application; protected OsmandSettings settings = null; private class FPSMeasurement { int fpsMeasureCount = 0; int fpsMeasureMs = 0; long fpsFirstMeasurement = 0; float fps; void calculateFPS(long start, long end) { fpsMeasureMs += end - start; fpsMeasureCount++; if (fpsMeasureCount > 10 || (start - fpsFirstMeasurement) > 400) { fpsFirstMeasurement = start; fps = (1000f * fpsMeasureCount / fpsMeasureMs); fpsMeasureCount = 0; fpsMeasureMs = 0; } } } protected static final int emptyTileDivisor = 16; public interface OnTrackBallListener { public boolean onTrackBallEvent(MotionEvent e); } public interface OnLongClickListener { public boolean onLongPressEvent(PointF point); } public interface OnClickListener { public boolean onPressEvent(PointF point); } protected static final Log LOG = PlatformUtil.getLog(OsmandMapTileView.class); private RotatedTileBox currentViewport; private float rotate; // accumulate private int mapPosition; private int mapPositionX; private boolean showMapPosition = true; private IMapLocationListener locationListener; private OnLongClickListener onLongClickListener; private OnClickListener onClickListener; private OnTrackBallListener trackBallDelegate; private AccessibilityActionsProvider accessibilityActions; private List<OsmandMapLayer> layers = new ArrayList<>(); private BaseMapLayer mainLayer; private Map<OsmandMapLayer, Float> zOrders = new HashMap<OsmandMapLayer, Float>(); // UI Part // handler to refresh map (in ui thread - ui thread is not necessary, but msg queue is required). protected Handler handler; private Handler baseHandler; private AnimateDraggingMapThread animatedDraggingThread; Paint paintGrayFill; Paint paintBlackFill; Paint paintWhiteFill; Paint paintCenter; private DisplayMetrics dm; private MapRendererView mapRenderer; private Bitmap bufferBitmap; private RotatedTileBox bufferImgLoc; private Bitmap bufferBitmapTmp; private Paint paintImg; private GestureDetector gestureDetector; private MultiTouchSupport multiTouchSupport; private DoubleTapScaleDetector doubleTapScaleDetector; private TwoFingerTapDetector twoFingersTapDetector; //private boolean afterTwoFingersTap = false; private boolean afterDoubleTap = false; private boolean wasMapLinkedBeforeGesture = false; public OsmandMapTileView(MapActivity activity, int w, int h) { this.activity = activity; init(activity, w, h); } // ///////////////////////////// INITIALIZING UI PART /////////////////////////////////// public void init(final MapActivity ctx, int w, int h) { application = (OsmandApplication) ctx.getApplicationContext(); settings = application.getSettings(); paintGrayFill = new Paint(); paintGrayFill.setColor(Color.GRAY); paintGrayFill.setStyle(Style.FILL); // when map rotate paintGrayFill.setAntiAlias(true); paintBlackFill = new Paint(); paintBlackFill.setColor(Color.BLACK); paintBlackFill.setStyle(Style.FILL); // when map rotate paintBlackFill.setAntiAlias(true); paintWhiteFill = new Paint(); paintWhiteFill.setColor(Color.WHITE); paintWhiteFill.setStyle(Style.FILL); // when map rotate paintWhiteFill.setAntiAlias(true); paintCenter = new Paint(); paintCenter.setStyle(Style.STROKE); paintCenter.setColor(Color.rgb(60, 60, 60)); paintCenter.setStrokeWidth(2); paintCenter.setAntiAlias(true); paintImg = new Paint(); paintImg.setFilterBitmap(true); // paintImg.setDither(true); handler = new Handler(); baseHandler = new Handler(application.getResourceManager().getRenderingBufferImageThread().getLooper()); animatedDraggingThread = new AnimateDraggingMapThread(this); WindowManager mgr = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE); dm = new DisplayMetrics(); mgr.getDefaultDisplay().getMetrics(dm); LatLon ll = settings.getLastKnownMapLocation(); currentViewport = new RotatedTileBox.RotatedTileBoxBuilder(). setLocation(ll.getLatitude(), ll.getLongitude()).setZoom(settings.getLastKnownMapZoom()). setPixelDimensions(w, h).build(); currentViewport.setDensity(dm.density); currentViewport.setMapDensity(getSettingsMapDensity()); gestureDetector = new GestureDetector(ctx, new MapTileViewOnGestureListener()); multiTouchSupport = new MultiTouchSupport(ctx, new MapTileViewMultiTouchZoomListener()); doubleTapScaleDetector = new DoubleTapScaleDetector(this, new MapTileViewMultiTouchZoomListener()); twoFingersTapDetector = new TwoFingerTapDetector() { @Override public void onTwoFingerTap() { //afterTwoFingersTap = true; if (isZoomingAllowed(getZoom(), -1.1f)) { getAnimatedDraggingThread().startZooming(getZoom() - 1, currentViewport.getZoomFloatPart(), false); if (wasMapLinkedBeforeGesture) { ctx.getMapViewTrackingUtilities().setMapLinkedToLocation(true); } } } }; } public void setView(View view) { this.view = view; view.setClickable(true); view.setLongClickable(true); view.setFocusable(true); refreshMap(true); } public Boolean onKeyDown(int keyCode, KeyEvent event) { return application.accessibilityEnabled() ? false : null; } public boolean isLayerVisible(OsmandMapLayer layer) { return layers.contains(layer); } public float getZorder(OsmandMapLayer layer) { Float z = zOrders.get(layer); if (z == null) { return 10; } return z; } public synchronized void addLayer(OsmandMapLayer layer, float zOrder) { int i = 0; for (i = 0; i < layers.size(); i++) { if (zOrders.get(layers.get(i)) > zOrder) { break; } } layer.initLayer(this); layers.add(i, layer); zOrders.put(layer, zOrder); } public synchronized void removeLayer(OsmandMapLayer layer) { while (layers.remove(layer)) ; zOrders.remove(layer); layer.destroyLayer(); } public synchronized void removeAllLayers() { while (layers.size() > 0) { removeLayer(layers.get(0)); } } public List<OsmandMapLayer> getLayers() { return layers; } @SuppressWarnings("unchecked") public <T extends OsmandMapLayer> T getLayerByClass(Class<T> cl) { for (OsmandMapLayer lr : layers) { if (cl.isInstance(lr)) { return (T) lr; } } return null; } public int getViewHeight() { if (view != null) { return view.getHeight(); } else { return 0; } } public OsmandApplication getApplication() { return application; } // ///////////////////////// NON UI PART (could be extracted in common) ///////////////////////////// public void setIntZoom(int zoom) { zoom = zoom > getMaxZoom() ? getMaxZoom() : zoom; zoom = zoom < getMinZoom() ? getMinZoom() : zoom; if (mainLayer != null) { animatedDraggingThread.stopAnimating(); currentViewport.setZoomAndAnimation(zoom, 0, 0); currentViewport.setRotate(zoom > LOWEST_ZOOM_TO_ROTATE ? rotate : 0); refreshMap(); } } public void setComplexZoom(int zoom, double mapDensity) { if (mainLayer != null && zoom <= getMaxZoom() && zoom >= getMinZoom()) { animatedDraggingThread.stopAnimating(); currentViewport.setZoomAndAnimation(zoom, 0); currentViewport.setMapDensity(mapDensity); currentViewport.setRotate(zoom > LOWEST_ZOOM_TO_ROTATE ? rotate : 0); refreshMap(); } } public boolean isMapRotateEnabled() { return getZoom() > LOWEST_ZOOM_TO_ROTATE; } public void setRotate(float rotate) { if (isMapRotateEnabled()) { float diff = MapUtils.unifyRotationDiff(rotate, getRotate()); if (Math.abs(diff) > 5) { // check smallest rotation animatedDraggingThread.startRotate(rotate); } } } public boolean isShowMapPosition() { return showMapPosition; } public void setShowMapPosition(boolean showMapPosition) { this.showMapPosition = showMapPosition; } public float getRotate() { return currentViewport.getRotate(); } public void setLatLon(double latitude, double longitude) { animatedDraggingThread.stopAnimating(); currentViewport.setLatLonCenter(latitude, longitude); refreshMap(); } public double getLatitude() { return currentViewport.getLatitude(); } public double getLongitude() { return currentViewport.getLongitude(); } public int getZoom() { return currentViewport.getZoom(); } public double getZoomFractionalPart() { return currentViewport.getZoomFloatPart(); } public double getSettingsMapDensity() { return (getSettings().MAP_DENSITY.get()) * Math.max(1, getDensity()); } public boolean isZooming() { return currentViewport.isZoomAnimated(); } public void setMapLocationListener(IMapLocationListener l) { locationListener = l; } /** * Adds listener to control when map is dragging */ public IMapLocationListener setMapLocationListener() { return locationListener; } // ////////////////////////////// DRAWING MAP PART ///////////////////////////////////////////// public BaseMapLayer getMainLayer() { return mainLayer; } public void setMainLayer(BaseMapLayer mainLayer) { this.mainLayer = mainLayer; int zoom = currentViewport.getZoom(); if (getMaxZoom() < zoom) { zoom = getMaxZoom(); } if (getMinZoom() > zoom) { zoom = getMinZoom(); } currentViewport.setZoomAndAnimation(zoom, 0, 0); refreshMap(); } public int getMapPosition() { return mapPosition; } public void setMapPosition(int type) { this.mapPosition = type; } public void setMapPositionX(int type) { this.mapPositionX = type; } public OsmandSettings getSettings() { return settings; } public int getMaxZoom() { if (mainLayer != null) { return mainLayer.getMaximumShownMapZoom(); } return BaseMapLayer.DEFAULT_MAX_ZOOM; } public int getMinZoom() { if (mainLayer != null) { return mainLayer.getMinimumShownMapZoom() + 1; } return BaseMapLayer.DEFAULT_MIN_ZOOM; } private void drawBasemap(Canvas canvas) { if (bufferImgLoc != null) { float rot = -bufferImgLoc.getRotate(); canvas.rotate(rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); final RotatedTileBox calc = currentViewport.copy(); calc.setRotate(bufferImgLoc.getRotate()); int cz = getZoom(); QuadPointDouble lt = bufferImgLoc.getLeftTopTile(cz); QuadPointDouble rb = bufferImgLoc.getRightBottomTile(cz); final float x1 = calc.getPixXFromTile(lt.x, lt.y, cz); final float x2 = calc.getPixXFromTile(rb.x, rb.y, cz); final float y1 = calc.getPixYFromTile(lt.x, lt.y, cz); final float y2 = calc.getPixYFromTile(rb.x, rb.y, cz); // LatLon lt = bufferImgLoc.getLeftTopLatLon(); // LatLon rb = bufferImgLoc.getRightBottomLatLon(); // final float x1 = calc.getPixXFromLatLon(lt.getLatitude(), lt.getLongitude()); // final float x2 = calc.getPixXFromLatLon(rb.getLatitude(), rb.getLongitude()); // final float y1 = calc.getPixYFromLatLon(lt.getLatitude(), lt.getLongitude()); // final float y2 = calc.getPixYFromLatLon(rb.getLatitude(), rb.getLongitude()); if (!bufferBitmap.isRecycled()) { RectF rct = new RectF(x1, y1, x2, y2); canvas.drawBitmap(bufferBitmap, null, rct, paintImg); } canvas.rotate(-rot, currentViewport.getCenterPixelX(), currentViewport.getCenterPixelY()); } } private void refreshBaseMapInternal(RotatedTileBox tileBox, DrawSettings drawSettings) { if (tileBox.getPixHeight() == 0 || tileBox.getPixWidth() == 0) { return; } if (bufferBitmapTmp == null || tileBox.getPixHeight() != bufferBitmapTmp.getHeight() || tileBox.getPixWidth() != bufferBitmapTmp.getWidth()) { bufferBitmapTmp = Bitmap.createBitmap(tileBox.getPixWidth(), tileBox.getPixHeight(), Config.RGB_565); } long start = SystemClock.elapsedRealtime(); final QuadPoint c = tileBox.getCenterPixelPoint(); Canvas canvas = new Canvas(bufferBitmapTmp); fillCanvas(canvas, drawSettings); for (int i = 0; i < layers.size(); i++) { try { OsmandMapLayer layer = layers.get(i); canvas.save(); // rotate if needed if (!layer.drawInScreenPixels()) { canvas.rotate(tileBox.getRotate(), c.x, c.y); } layer.onPrepareBufferImage(canvas, tileBox, drawSettings); canvas.restore(); } catch (IndexOutOfBoundsException e) { // skip it canvas.restore(); } } Bitmap t = bufferBitmap; synchronized (this) { bufferImgLoc = tileBox; bufferBitmap = bufferBitmapTmp; bufferBitmapTmp = t; } long end = SystemClock.elapsedRealtime(); additional.calculateFPS(start, end); } private void refreshMapInternal(DrawSettings drawSettings) { if (view == null) { return; } final float ratioy; if (mapPosition == OsmandSettings.BOTTOM_CONSTANT) { ratioy = 0.85f; } else if (mapPosition == OsmandSettings.MIDDLE_CONSTANT) { ratioy = 0.70f; } else { ratioy = 0.5f; } final float ratiox = mapPositionX == 0 ? 0.5f : 0.75f; final int cy = (int) (ratioy * view.getHeight()); final int cx = (int) (ratiox * view.getWidth()); if (currentViewport.getPixWidth() != view.getWidth() || currentViewport.getPixHeight() != view.getHeight() || currentViewport.getCenterPixelY() != cy || currentViewport.getCenterPixelX() != cx) { currentViewport.setPixelDimensions(view.getWidth(), view.getHeight(), ratiox, ratioy); refreshBufferImage(drawSettings); } if (view instanceof SurfaceView) { SurfaceHolder holder = ((SurfaceView) view).getHolder(); long ms = SystemClock.elapsedRealtime(); synchronized (holder) { Canvas canvas = holder.lockCanvas(); if (canvas != null) { try { // make copy to avoid concurrency RotatedTileBox viewportToDraw = currentViewport.copy(); drawOverMap(canvas, viewportToDraw, drawSettings); } finally { holder.unlockCanvasAndPost(canvas); } } if (MEASURE_FPS) { main.calculateFPS(ms, SystemClock.elapsedRealtime()); } } } else { view.invalidate(); } } private void fillCanvas(Canvas canvas, DrawSettings drawSettings) { if (drawSettings.isNightMode()) { canvas.drawARGB(255, 100, 100, 100); } else { canvas.drawARGB(255, 225, 225, 225); } } public boolean isMeasureFPS() { return MEASURE_FPS; } public void setMeasureFPS(boolean measureFPS) { MEASURE_FPS = measureFPS; } public float getFPS() { return main.fps; } public float getSecondaryFPS() { return additional.fps; } @SuppressLint("WrongCall") public void drawOverMap(Canvas canvas, RotatedTileBox tileBox, DrawSettings drawSettings) { if (mapRenderer == null) { fillCanvas(canvas, drawSettings); } final QuadPoint c = tileBox.getCenterPixelPoint(); synchronized (this) { if (bufferBitmap != null && !bufferBitmap.isRecycled() && mapRenderer == null) { canvas.save(); canvas.rotate(tileBox.getRotate(), c.x, c.y); drawBasemap(canvas); canvas.restore(); } } for (int i = 0; i < layers.size(); i++) { try { OsmandMapLayer layer = layers.get(i); canvas.save(); // rotate if needed if (!layer.drawInScreenPixels()) { canvas.rotate(tileBox.getRotate(), c.x, c.y); } if (mapRenderer != null) { layer.onPrepareBufferImage(canvas, tileBox, drawSettings); } layer.onDraw(canvas, tileBox, drawSettings); canvas.restore(); } catch (IndexOutOfBoundsException e) { // skip it } } if (showMapPosition || animatedDraggingThread.isAnimatingZoom()) { drawMapPosition(canvas, c.x, c.y); } else if (multiTouchSupport.isInZoomMode()) { drawMapPosition(canvas, multiTouchSupport.getCenterPoint().x, multiTouchSupport.getCenterPoint().y); } else if (doubleTapScaleDetector.isInZoomMode()) { drawMapPosition(canvas, doubleTapScaleDetector.getCenterX(), doubleTapScaleDetector.getCenterY()); } } protected void drawMapPosition(Canvas canvas, float x, float y) { canvas.drawCircle(x, y, 3 * dm.density, paintCenter); canvas.drawCircle(x, y, 7 * dm.density, paintCenter); } private void refreshBufferImage(final DrawSettings drawSettings) { if (mapRenderer != null) { return; } if (!baseHandler.hasMessages(BASE_REFRESH_MESSAGE) || drawSettings.isUpdateVectorRendering()) { Message msg = Message.obtain(baseHandler, new Runnable() { @Override public void run() { baseHandler.removeMessages(BASE_REFRESH_MESSAGE); try { DrawSettings param = drawSettings; if (handler.hasMessages(MAP_FORCE_REFRESH_MESSAGE)) { if (!param.isUpdateVectorRendering()) { param = new DrawSettings(drawSettings.isNightMode(), true); } handler.removeMessages(MAP_FORCE_REFRESH_MESSAGE); } refreshBaseMapInternal(currentViewport.copy(), param); sendRefreshMapMsg(param, 0); } catch (Exception e) { LOG.error(e.getMessage(), e); } } }); msg.what = drawSettings.isUpdateVectorRendering() ? MAP_FORCE_REFRESH_MESSAGE : BASE_REFRESH_MESSAGE; // baseHandler.sendMessageDelayed(msg, 0); baseHandler.sendMessage(msg); } } // this method could be called in non UI thread public void refreshMap() { refreshMap(false); } // this method could be called in non UI thread public void refreshMap(final boolean updateVectorRendering) { if (view != null && view.isShown()) { boolean nightMode = application.getDaynightHelper().isNightMode(); DrawSettings drawSettings = new DrawSettings(nightMode, updateVectorRendering); sendRefreshMapMsg(drawSettings, 20); refreshBufferImage(drawSettings); } } private void sendRefreshMapMsg(final DrawSettings drawSettings, int delay) { if (!handler.hasMessages(MAP_REFRESH_MESSAGE) || drawSettings.isUpdateVectorRendering()) { Message msg = Message.obtain(handler, new Runnable() { @Override public void run() { DrawSettings param = drawSettings; handler.removeMessages(MAP_REFRESH_MESSAGE); refreshMapInternal(param); } }); msg.what = MAP_REFRESH_MESSAGE; if (delay > 0) { handler.sendMessageDelayed(msg, delay); } else { handler.sendMessage(msg); } } } @Override public void tileDownloaded(DownloadRequest request) { // force to refresh map because image can be loaded from different threads // and threads can block each other especially for sqlite images when they // are inserting into db they block main thread refreshMap(); } // ///////////////////////////////// DRAGGING PART /////////////////////////////////////// public net.osmand.data.RotatedTileBox getCurrentRotatedTileBox() { return currentViewport; } public float getDensity() { return currentViewport.getDensity(); } public float getScaleCoefficient() { float scaleCoefficient = getDensity(); if (Math.min(dm.widthPixels / (dm.density * 160), dm.heightPixels / (dm.density * 160)) > 2.5f) { // large screen scaleCoefficient *= 1.5f; } return scaleCoefficient; } /** * These methods do not consider rotating */ protected void dragToAnimate(float fromX, float fromY, float toX, float toY, boolean notify) { float dx = (fromX - toX); float dy = (fromY - toY); moveTo(dx, dy); if (locationListener != null && notify) { locationListener.locationChanged(getLatitude(), getLongitude(), this); } } protected void rotateToAnimate(float rotate) { if (isMapRotateEnabled()) { this.rotate = MapUtils.unifyRotationTo360(rotate); currentViewport.setRotate(this.rotate); refreshMap(); } } protected void setLatLonAnimate(double latitude, double longitude, boolean notify) { currentViewport.setLatLonCenter(latitude, longitude); refreshMap(); if (locationListener != null && notify) { locationListener.locationChanged(latitude, longitude, this); } } protected void setFractionalZoom(int zoom, double zoomPart, boolean notify) { currentViewport.setZoomAndAnimation(zoom, 0, zoomPart); refreshMap(); if (locationListener != null && notify) { locationListener.locationChanged(getLatitude(), getLongitude(), this); } } // for internal usage protected void zoomToAnimate(int zoom, double zoomToAnimate, boolean notify) { if (mainLayer != null && getMaxZoom() >= zoom && getMinZoom() <= zoom) { currentViewport.setZoomAndAnimation(zoom, zoomToAnimate); currentViewport.setRotate(zoom > LOWEST_ZOOM_TO_ROTATE ? rotate : 0); refreshMap(); if (notify && locationListener != null) { locationListener.locationChanged(getLatitude(), getLongitude(), this); } } } public void moveTo(float dx, float dy) { final QuadPoint cp = currentViewport.getCenterPixelPoint(); final LatLon latlon = currentViewport.getLatLonFromPixel(cp.x + dx, cp.y + dy); currentViewport.setLatLonCenter(latlon.getLatitude(), latlon.getLongitude()); refreshMap(); // do not notify here listener } public void fitRectToMap(double left, double right, double top, double bottom, int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx) { RotatedTileBox tb = currentViewport.copy(); double border = 0.8; int dy = 0; int tbw = (int) (tb.getPixWidth() * border); int tbh = (int) (tb.getPixHeight() * border); if (tileBoxWidthPx > 0) { tbw = (int) (tileBoxWidthPx * border); } else if (tileBoxHeightPx > 0) { tbh = (int) (tileBoxHeightPx * border); dy = (tb.getPixHeight() - tileBoxHeightPx) / 2 - marginTopPx; } tb.setPixelDimensions(tbw, tbh); double clat = bottom / 2 + top / 2; double clon = left / 2 + right / 2; tb.setLatLonCenter(clat, clon); while (tb.getZoom() < 17 && tb.containsLatLon(top, left) && tb.containsLatLon(bottom, right)) { tb.setZoom(tb.getZoom() + 1); } while (tb.getZoom() >= 7 && (!tb.containsLatLon(top, left) || !tb.containsLatLon(bottom, right))) { tb.setZoom(tb.getZoom() - 1); } if (dy != 0) { clat = tb.getLatFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2 + dy); clon = tb.getLonFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2); } animatedDraggingThread.startMoving(clat, clon, tb.getZoom(), true); } public RotatedTileBox getTileBox(int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx) { RotatedTileBox tb = currentViewport.copy(); double border = 0.8; int dy = 0; int tbw = (int) (tb.getPixWidth() * border); int tbh = (int) (tb.getPixHeight() * border); if (tileBoxWidthPx > 0) { tbw = (int) (tileBoxWidthPx * border); } else if (tileBoxHeightPx > 0) { tbh = (int) (tileBoxHeightPx * border); dy = (tb.getPixHeight() - tileBoxHeightPx) / 2 - marginTopPx; } tb.setPixelDimensions(tbw, tbh); if (dy != 0) { double clat = tb.getLatFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2 - dy); double clon = tb.getLonFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2); tb.setLatLonCenter(clat, clon); } return tb; } public void fitLocationToMap(double clat, double clon, int zoom, int tileBoxWidthPx, int tileBoxHeightPx, int marginTopPx, boolean animated) { RotatedTileBox tb = currentViewport.copy(); int dy = 0; int tbw = tb.getPixWidth(); int tbh = tb.getPixHeight(); if (tileBoxWidthPx > 0) { tbw = tileBoxWidthPx; } else if (tileBoxHeightPx > 0) { tbh = tileBoxHeightPx; dy = (tb.getPixHeight() - tileBoxHeightPx) / 2 - marginTopPx; } tb.setPixelDimensions(tbw, tbh); tb.setLatLonCenter(clat, clon); tb.setZoom(zoom); if (dy != 0) { clat = tb.getLatFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2 + dy); clon = tb.getLonFromPixel(tb.getPixWidth() / 2, tb.getPixHeight() / 2); } if (animated) { animatedDraggingThread.startMoving(clat, clon, tb.getZoom(), true); } else { setLatLon(clat, clon); } } public boolean onGenericMotionEvent(MotionEvent event) { if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0 && event.getAction() == MotionEvent.ACTION_SCROLL && event.getAxisValue(MotionEvent.AXIS_VSCROLL) != 0) { final RotatedTileBox tb = getCurrentRotatedTileBox(); final double lat = tb.getLatFromPixel(event.getX(), event.getY()); final double lon = tb.getLonFromPixel(event.getX(), event.getY()); int zoomDir = event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0 ? -1 : 1; getAnimatedDraggingThread().startMoving(lat, lon, getZoom() + zoomDir, true); return true; } return false; } public boolean onTouchEvent(MotionEvent event) { if (mapRenderer != null) { if (event.getAction() == MotionEvent.ACTION_DOWN) { mapRenderer.suspendSymbolsUpdate(); } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { mapRenderer.resumeSymbolsUpdate(); } } if (twoFingersTapDetector.onTouchEvent(event)) { ContextMenuLayer contextMenuLayer = getLayerByClass(ContextMenuLayer.class); if (contextMenuLayer != null) { contextMenuLayer.onTouchEvent(event, getCurrentRotatedTileBox()); } return true; } if (event.getAction() == MotionEvent.ACTION_DOWN) { animatedDraggingThread.stopAnimating(); } final boolean isMultiTouch = multiTouchSupport.onTouchEvent(event); doubleTapScaleDetector.onTouchEvent(event); if (!isMultiTouch && !doubleTapScaleDetector.isInZoomMode()) { for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).onTouchEvent(event, getCurrentRotatedTileBox()); } } if (!doubleTapScaleDetector.isInZoomMode() && !doubleTapScaleDetector.isDoubleTapping()) { gestureDetector.onTouchEvent(event); } return true; } public void setMapRender(MapRendererView mapRenderer) { this.mapRenderer = mapRenderer; } public MapRendererView getMapRenderer() { return mapRenderer; } public Boolean onTrackballEvent(MotionEvent event) { if (trackBallDelegate != null) { return trackBallDelegate.onTrackBallEvent(event); } return null; } public void setTrackBallDelegate(OnTrackBallListener trackBallDelegate) { this.trackBallDelegate = trackBallDelegate; } public void setOnLongClickListener(OnLongClickListener l) { this.onLongClickListener = l; } public void setOnClickListener(OnClickListener l) { this.onClickListener = l; } public void setAccessibilityActions(AccessibilityActionsProvider actions) { accessibilityActions = actions; } public AnimateDraggingMapThread getAnimatedDraggingThread() { return animatedDraggingThread; } public void showMessage(final String msg) { handler.post(new Runnable() { @Override public void run() { Toast.makeText(application, msg, Toast.LENGTH_SHORT).show(); //$NON-NLS-1$ } }); } private class MapTileViewMultiTouchZoomListener implements MultiTouchZoomListener, DoubleTapScaleDetector.DoubleTapZoomListener { private PointF initialMultiTouchCenterPoint; private RotatedTileBox initialViewport; private float x1; private float y1; private float x2; private float y2; private LatLon initialCenterLatLon; private boolean startRotating = false; private static final float ANGLE_THRESHOLD = 30; @Override public void onZoomOrRotationEnded(double relativeToStart, float angleRelative) { // 1.5 works better even on dm.density=1 devices float dz = (float) (Math.log(relativeToStart) / Math.log(2)) * 1.5f; setIntZoom(Math.round(dz) + initialViewport.getZoom()); if (Math.abs(angleRelative) < ANGLE_THRESHOLD * relativeToStart || Math.abs(angleRelative) < ANGLE_THRESHOLD / relativeToStart) { angleRelative = 0; } rotateToAnimate(initialViewport.getRotate() + angleRelative); final int newZoom = getZoom(); if (application.accessibilityEnabled()) { if (newZoom != initialViewport.getZoom()) { showMessage(application.getString(R.string.zoomIs) + " " + newZoom); //$NON-NLS-1$ } else { final LatLon p1 = initialViewport.getLatLonFromPixel(x1, y1); final LatLon p2 = initialViewport.getLatLonFromPixel(x2, y2); showMessage(OsmAndFormatter.getFormattedDistance((float) MapUtils.getDistance(p1.getLatitude(), p1.getLongitude(), p2.getLatitude(), p2.getLongitude()), application)); } } } @Override public void onZoomEnded(double relativeToStart) { // 1.5 works better even on dm.density=1 devices float dz = (float) ((relativeToStart - 1) * DoubleTapScaleDetector.SCALE_PER_SCREEN); setIntZoom(Math.round(dz) + initialViewport.getZoom()); final int newZoom = getZoom(); if (application.accessibilityEnabled()) { if (newZoom != initialViewport.getZoom()) { showMessage(application.getString(R.string.zoomIs) + " " + newZoom); //$NON-NLS-1$ } else { final LatLon p1 = initialViewport.getLatLonFromPixel(x1, y1); final LatLon p2 = initialViewport.getLatLonFromPixel(x2, y2); showMessage(OsmAndFormatter.getFormattedDistance((float) MapUtils.getDistance(p1.getLatitude(), p1.getLongitude(), p2.getLatitude(), p2.getLongitude()), application)); } } } @Override public void onGestureInit(float x1, float y1, float x2, float y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; } @Override public void onZoomStarted(PointF centerPoint) { initialMultiTouchCenterPoint = centerPoint; initialViewport = getCurrentRotatedTileBox().copy(); initialCenterLatLon = initialViewport.getLatLonFromPixel(initialMultiTouchCenterPoint.x, initialMultiTouchCenterPoint.y); startRotating = false; } @Override public void onZoomingOrRotating(double relativeToStart, float relAngle) { double dz = (Math.log(relativeToStart) / Math.log(2)) * 1.5; if (Math.abs(dz) <= 0.1) { // keep only rotating dz = 0; } if (Math.abs(relAngle) < ANGLE_THRESHOLD && !startRotating) { relAngle = 0; } else { startRotating = true; } if (dz != 0 || relAngle != 0) { changeZoomPosition((float) dz, relAngle); } } @Override public void onZooming(double relativeToStart) { double dz = (relativeToStart - 1) * DoubleTapScaleDetector.SCALE_PER_SCREEN; changeZoomPosition((float) dz, 0); } @Override public boolean onDoubleTap(MotionEvent e) { LOG.debug("onDoubleTap getZoom()"); if (!doubleTapScaleDetector.isInZoomMode()) { if (isZoomingAllowed(getZoom(), 1.1f)) { final RotatedTileBox tb = getCurrentRotatedTileBox(); final double lat = tb.getLatFromPixel(e.getX(), e.getY()); final double lon = tb.getLonFromPixel(e.getX(), e.getY()); getAnimatedDraggingThread().startMoving(lat, lon, getZoom() + 1, true); } afterDoubleTap = true; return true; } else { return false; } } private void changeZoomPosition(float dz, float angle) { final RotatedTileBox calc = initialViewport.copy(); calc.setLatLonCenter(initialCenterLatLon.getLatitude(), initialCenterLatLon.getLongitude()); float calcRotate = calc.getRotate() + angle; calc.setRotate(calcRotate); calc.setZoomAndAnimation(initialViewport.getZoom(), dz, initialViewport.getZoomFloatPart()); final QuadPoint cp = initialViewport.getCenterPixelPoint(); // Keep zoom center fixed or flexible LatLon r; if (multiTouchSupport.isInZoomMode()) { r = calc.getLatLonFromPixel(cp.x + cp.x - multiTouchSupport.getCenterPoint().x, cp.y + cp.y - multiTouchSupport.getCenterPoint().y); } else { r = calc.getLatLonFromPixel(cp.x + cp.x - initialMultiTouchCenterPoint.x, cp.y + cp.y - initialMultiTouchCenterPoint.y); } setLatLon(r.getLatitude(), r.getLongitude()); int baseZoom = initialViewport.getZoom(); while (initialViewport.getZoomFloatPart() + dz > 1 && isZoomingAllowed(baseZoom, dz)) { dz--; baseZoom++; } while (initialViewport.getZoomFloatPart() + dz < 0 && isZoomingAllowed(baseZoom, dz)) { dz++; baseZoom--; } if (!isZoomingAllowed(baseZoom, dz)) { dz = Math.signum(dz); } zoomToAnimate(baseZoom, dz, !(doubleTapScaleDetector.isInZoomMode())); rotateToAnimate(calcRotate); } } private boolean isZoomingAllowed(int baseZoom, float dz) { if (baseZoom > getMaxZoom()) { return false; } if (baseZoom > getMaxZoom() - 1 && dz > 1) { return false; } if (baseZoom < getMinZoom()) { return false; } if (baseZoom < getMinZoom() + 1 && dz < -1) { return false; } return true; } private class MapTileViewOnGestureListener extends SimpleOnGestureListener { @Override public boolean onDown(MotionEvent e) { // Facilitates better map re-linking for two finger tap zoom out wasMapLinkedBeforeGesture = ((MapActivity) activity).getMapViewTrackingUtilities().isMapLinkedToLocation(); return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { animatedDraggingThread.startDragging(velocityX / 3, velocityY / 3, e1.getX(), e1.getY(), e2.getX(), e2.getY(), true); return true; } @Override public void onLongPress(MotionEvent e) { if (multiTouchSupport.isInZoomMode() || doubleTapScaleDetector.isInZoomMode() || doubleTapScaleDetector.isDoubleTapping()) { // || afterTwoFingersTap) { //afterTwoFingersTap = false; return; } if (LOG.isDebugEnabled()) { LOG.debug("On long click event " + e.getX() + " " + e.getY()); //$NON-NLS-1$ //$NON-NLS-2$ } PointF point = new PointF(e.getX(), e.getY()); if ((accessibilityActions != null) && accessibilityActions.onLongClick(point, getCurrentRotatedTileBox())) { return; } for (int i = layers.size() - 1; i >= 0; i--) { if (layers.get(i).onLongPressEvent(point, getCurrentRotatedTileBox())) { return; } } if (onLongClickListener != null && onLongClickListener.onLongPressEvent(point)) { return; } } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { dragToAnimate(e2.getX() + distanceX, e2.getY() + distanceY, e2.getX(), e2.getY(), true); return true; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (doubleTapScaleDetector.isDoubleTapping() || afterDoubleTap) { // Needed to suppress false single tap detection if we mask MotionEvents for gestures on isDoubleTapping() afterDoubleTap = false; return true; } PointF point = new PointF(e.getX(), e.getY()); if (LOG.isDebugEnabled()) { LOG.debug("On click event " + point.x + " " + point.y); //$NON-NLS-1$ //$NON-NLS-2$ } if ((accessibilityActions != null) && accessibilityActions.onClick(point, getCurrentRotatedTileBox())) { return true; } for (int i = layers.size() - 1; i >= 0; i--) { if (layers.get(i).onSingleTap(point, getCurrentRotatedTileBox())) { return true; } } if (onClickListener != null && onClickListener.onPressEvent(point)) { return true; } return false; } } public Resources getResources() { return application.getResources(); } public Context getContext() { return activity; } }